package com.core.snowflake.comm;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Random;

/**
 * AES加解密工具
 * 参考
 * https://blog.csdn.net/fuhanghang/article/details/124010261
 * @author 苏斌
 *
 */
public class AES {
	/**
	 * 【加密模式及填充方式】
	 * 格式: 算法/模式/补码方式
	 *
	 *   PKCS5Padding（默认）
	 *     在明文的末尾进行填充，填充的数据是当前和16个字节相差的数量，
	 *
	 *  ECB模式（默认）电码本模式 Electronic Codebook Book
	 *     这个模式是默认的，就只是根据密钥的位数，将数据分成不同的块进行加密，加密完成后，再将加密后的数据拼接起来
	 *     优点：简单、速度快、可并行
	 *     缺点：如果明文块相同，则生成的密文块也相同，这样会导致安全性降低
	 *
	 *  CBC模式   密码分组链接模式 Cipher Block Chaining
	 *     为了解决ECB模式的密文块相同的缺点，CBC的模式引入了一个初始向量概念，
	 *     该向量必须是一个与密钥长度相等的数据，在第一次加密前，会使用初始化向量与第一块数据做异或运算，
	 *     生成的新数据再进行加密，加密第二块之前，会拿第一块的密文数据与第二块明文进行异或运算后再进行加密，
	 *     以此类推，解密时也是在解密后，进行异或运算，生成最终的明文
	 *
	 *     这里需要注意如下几点：
	 *     1.向量必须是一个与密钥长度相等的数据
	 *     2.由于在加密前和解密后都会做异或运算，因此我们的明文可以不用补全，不是16个字节的倍数也可以，CBC中会自动用0补全进行异或运算
	 *     3.在解密时是解密后才会再做异或运算，保证数据解密成功
	 *     4.由于自动进行了补全，所以解密出的数据也会在后面补全0，因此获取到数据时，需要将末尾的0去除，或者根据源数据长度来截取解密后的数据
	 *
	 *     优点：每次加密密钥不同，加强了安全性
	 *
	 *     CBC的方式解决了EBC的缺点，但是也有其缺点：
	 *     1.加密无法并行运算，但是解密可以并行，必须在前一个块加密完成后，才能加密后块，并且也需要填充0在后面，所以并不适合流数据（不适合的原因可能是，需要满足128位的数据之后才能进行加密，这样后面才不会有0的补全）
	 *     2.如果前一个数据加密错误，那么后续的数据都是错的了
	 *     3.两端需要同时约定初始向量iv
	 */
	//private final static String ALGORITHM_AEPP = "AES/ECB/PKCS5Padding";
	private static final String AES_CBC_PADDING = "AES/CBC/PKCS5Padding";//AES/CBC/PKCS7Padding
	//推荐使用
	private static final String AES_ECB_PADDING = "AES/ECB/PKCS5Padding";//AES/ECB/PKCS7Padding


	/** 秘钥生成来源 */
	private static final String ALLCHAR = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~!@#$%^&*";

	/**
	 * 加解密统一编码方式
	 */
	private final static Charset ENCODING = StandardCharsets.UTF_8;

	/**
	 * 随机生成AES密钥(16位)
	 */
	public static String generateAESKey()  {
		StringBuffer sb = new StringBuffer();
		Random random = new Random();
		for (int i = 0; i < 16; i++) {
			sb.append(ALLCHAR.charAt(random.nextInt(ALLCHAR.length())));
		}
		return sb.toString();
	}



	/**
	 * AES加密
	 *
	 * @param content
	 *            内容
	 * @param secret
	 *            密钥 长度16
	 * @return 加密后数据
	 */
	public static String encrypt(String content, String secret) throws Exception {
		if (content == null || secret == null)
			return null;
		// 判断Key是否为16位
		if (secret.length() != 16) {
			throw new Exception("密钥必须是16位");
		}

		String algorithm=AES_ECB_PADDING;
		try {
			Cipher cipher = null;
			if (algorithm.endsWith("PKCS7Padding")) {
				// AES加密采用PKCS7Padding填充
				cipher = Cipher.getInstance(algorithm, "BC");
			} else {
				//用密匙初始化Cipher对象 AES/ECB/PKCS5Padding
				cipher = Cipher.getInstance(algorithm);
			}
			cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret.getBytes(ENCODING), "AES"));
			return byte2Base64( cipher.doFinal(content.getBytes(ENCODING)));
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * AES解密
	 *
	 * @param content
	 *            加密内容
	 * @param secret
	 *            密钥 长度16
	 * @return 解密后数据
	 */
	public static String decrypt(String content, String secret) throws Exception {
		if (content == null || secret == null)
			return null;
		// 判断Key是否为16位
		if (secret.length() != 16) {
			throw new Exception("密钥必须是16位");
		}
		String algorithm=AES_ECB_PADDING;
		try {
			Cipher cipher = null;
			if (algorithm.endsWith("PKCS7Padding")) {
				cipher = Cipher.getInstance(algorithm, "BC");
			} else {
				cipher = Cipher.getInstance(algorithm);
			}
			cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret.getBytes(ENCODING), "AES"));
			byte[] bytes = cipher.doFinal(base642Byte(content));
			return new String(bytes, ENCODING);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * AES加密
	 * AES-128-CBC加密模式
	 * @param content
	 *            内容
	 * @param secret
	 *            密钥 长度16
	 * @param ivStr
	 *            向量 长度16 (必须和密钥长度一直)
	 * @return 加密后数据
	 */
	public static String encrypt(String content, String secret, String ivStr) throws Exception {
		if (content == null || secret == null)
			return null;
		// 判断Key是否为16位
		if (secret.length() != 16) {
			throw new Exception("密钥必须是16位");
		}
		String algorithm=AES_ECB_PADDING;

		try {
			Cipher cipher = null;
			if (algorithm.endsWith("PKCS7Padding")) {
				cipher = Cipher.getInstance(algorithm, "BC");
			} else {
				cipher = Cipher.getInstance(algorithm);
			}
			IvParameterSpec iv = new IvParameterSpec(ivStr.getBytes(ENCODING));
			cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret.getBytes(ENCODING), "AES"), iv);
			return byte2Base64( cipher.doFinal(content.getBytes(ENCODING)));
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * AES解密
	 *
	 * @param content
	 *            加密内容
	 * @param secret
	 *            密钥
	 * @param ivStr
	 *            向量 (必须和密钥长度一直)
	 * @return 解密后数据
	 */
	public static String decrypt(String content, String secret, String ivStr) throws Exception {
		if (content == null || secret == null)
			return null;
		// 判断Key是否为16位
		if (secret.length() != 16) {
			throw new Exception("密钥必须是16位");
		}
		String algorithm=AES_ECB_PADDING;

		try {
			Cipher cipher = null;
			if (algorithm.endsWith("PKCS7Padding")) {
				cipher = Cipher.getInstance(algorithm, "BC");
			} else {
				cipher = Cipher.getInstance(algorithm);
			}
			IvParameterSpec iv = new IvParameterSpec(ivStr.getBytes((StandardCharsets.UTF_8)));
			cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "AES"), iv);
			byte[] bytes = cipher.doFinal(base642Byte(content));
			return new String(bytes, ENCODING);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * 字节数组转Base64编码
	 *
	 * @param bytes
	 * @return
	 */
	private static String byte2Base64(byte[] bytes) {
		//BASE64Encoder encoder = new BASE64Encoder();
		return Base64.getEncoder().encodeToString(bytes);
	}

	/**
	 * Base64编码转字节数组
	 *
	 * @param base64Key
	 * @return
	 * @throws IOException
	 */
	private static byte[] base642Byte(String base64Key) throws IOException {
		return Base64.getDecoder().decode(base64Key);//  Base64.decodeBase64(bytes);
	}

}
